/* ========================================================================
 * Copyright (c) 2005-2016 The OPC Foundation, Inc. All rights reserved.
 *
 * OPC Foundation MIT License 1.00
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * The complete license agreement can be found here:
 * http://opcfoundation.org/License/MIT/1.00/
 * ======================================================================*/

using Opc.Ua;
using Opc.Ua.Server;
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;

namespace OpcUaServerSample
{
	/// <summary>
	/// Implements a basic SharpNodeSettings Server.
	/// </summary>
	/// <remarks>
	/// Each server instance must have one instance of a StandardServer object which is
	/// responsible for reading the configuration file, creating the endpoints and dispatching
	/// incoming requests to the appropriate handler.
	/// 
	/// This sub-class specifies non-configurable metadata such as Product Name and initializes
	/// the EmptyNodeManager which provides access to the data exposed by the Server.
	/// </remarks>
	public partial class SharpNodeSettingsServer : StandardServer
	{
		#region Overridden Methods
		/// <summary>
		/// Creates the node managers for the server.
		/// </summary>
		/// <remarks>
		/// This method allows the sub-class create any additional node managers which it uses. The SDK
		/// always creates a CoreNodeManager which handles the built-in nodes defined by the specification.
		/// Any additional NodeManagers are expected to handle application specific nodes.
		/// </remarks>
		protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration)
		{
			Utils.Trace("Creating the Node Managers.");

			List<INodeManager> nodeManagers = new List<INodeManager>();

			// create the custom node managers.
			nodeManagers.Add(new EmptyNodeManager(server, configuration));

			// create master node manager.
			return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray());
		}

		/// <summary>
		/// Loads the non-configurable properties for the application.
		/// </summary>
		/// <remarks>
		/// These properties are exposed by the server but cannot be changed by administrators.
		/// </remarks>
		protected override ServerProperties LoadServerProperties()
		{
			ServerProperties properties = new ServerProperties();

			properties.ManufacturerName = "Richard Hu";
			properties.ProductName = "SharpNodeSettingsServer";
			properties.ProductUri = "http://opcfoundation.org/Quickstart/ReferenceServer/v1.03";
			properties.SoftwareVersion = Utils.GetAssemblySoftwareVersion();
			properties.BuildNumber = Utils.GetAssemblyBuildNumber();
			properties.BuildDate = Utils.GetAssemblyTimestamp();

			// TBD - All applications have software certificates that need to added to the properties.

			return properties;
		}

		/// <summary>
		/// Creates the resource manager for the server.
		/// </summary>
		protected override ResourceManager CreateResourceManager(IServerInternal server, ApplicationConfiguration configuration)
		{
			ResourceManager resourceManager = new ResourceManager(server, configuration);

			System.Reflection.FieldInfo[] fields = typeof(StatusCodes).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);

			foreach (System.Reflection.FieldInfo field in fields)
			{
				uint? id = field.GetValue(typeof(StatusCodes)) as uint?;

				if (id != null)
				{
					resourceManager.Add(id.Value, "en-US", field.Name);
				}
			}

			return resourceManager;
		}

		/// <summary>
		/// Called after the server has been started.
		/// </summary>
		protected override void OnServerStarted(IServerInternal server)
		{
			base.OnServerStarted(server);

			// request notifications when the user identity is changed. all valid users are accepted by default.
			server.SessionManager.ImpersonateUser += new ImpersonateEventHandler(SessionManager_ImpersonateUser);
		}

		#endregion

		#region User Validation Functions
		/// <summary>
		/// Called when a client tries to change its user identity.
		/// </summary>
		private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArgs args)
		{
			// check for a user name token.
			UserNameIdentityToken userNameToken = args.NewIdentity as UserNameIdentityToken;

			if (userNameToken != null)
			{
				args.Identity = VerifyPassword(userNameToken);
				return;
			}

			// check for x509 user token.
			X509IdentityToken x509Token = args.NewIdentity as X509IdentityToken;

			if (x509Token != null)
			{
				VerifyUserTokenCertificate(x509Token.Certificate);
				args.Identity = new UserIdentity(x509Token);
				Utils.Trace("X509 Token Accepted: {0}", args.Identity.DisplayName);
				return;
			}
		}

		/// <summary>
		/// Validates the password for a username token.
		/// </summary>
		private IUserIdentity VerifyPassword(UserNameIdentityToken userNameToken)
		{
			var userName = userNameToken.UserName;
			var password = userNameToken.DecryptedPassword;
			if (String.IsNullOrEmpty(userName))
			{
				// an empty username is not accepted.
				throw ServiceResultException.Create(StatusCodes.BadIdentityTokenInvalid,
					"Security token is not a valid username token. An empty username is not accepted.");
			}

			if (String.IsNullOrEmpty(password))
			{
				// an empty password is not accepted.
				throw ServiceResultException.Create(StatusCodes.BadIdentityTokenRejected,
					"Security token is not a valid username token. An empty password is not accepted.");
			}

			// User with permission to configure server
			if (userName == "sysadmin" && password == "demo")
			{
				return new SystemConfigurationIdentity(new UserIdentity(userNameToken));
			}

			// standard users for CTT verification
			if (!((userName == "user1" && password == "password") ||
				(userName == "user2" && password == "password1")))
			{
				// construct translation object with default text.
				TranslationInfo info = new TranslationInfo(
					"InvalidPassword",
					"en-US",
					"Invalid username or password.",
					userName);

				// create an exception with a vendor defined sub-code.
				throw new ServiceResultException(new ServiceResult(
					StatusCodes.BadUserAccessDenied,
					"InvalidPassword",
					LoadServerProperties().ProductUri,
					new LocalizedText(info)));
			}

			return new UserIdentity(userNameToken);
		}

		/// <summary>
		/// Verifies that a certificate user token is trusted.
		/// </summary>
		private void VerifyUserTokenCertificate(X509Certificate2 certificate)
		{
			try
			{
				CertificateValidator.Validate(certificate);

				// determine if self-signed.
				bool isSelfSigned = Utils.CompareDistinguishedName(certificate.Subject, certificate.Issuer);

				// do not allow self signed application certs as user token
				//if (isSelfSigned && Utils.HasApplicationURN(certificate))
				//{
				//    throw new ServiceResultException(StatusCodes.BadCertificateUseNotAllowed);
				//}
			}
			catch (Exception e)
			{
				TranslationInfo info;
				StatusCode result = StatusCodes.BadIdentityTokenRejected;
				ServiceResultException se = e as ServiceResultException;
				if (se != null && se.StatusCode == StatusCodes.BadCertificateUseNotAllowed)
				{
					info = new TranslationInfo(
						"InvalidCertificate",
						"en-US",
						"'{0}' is an invalid user certificate.",
						certificate.Subject);

					result = StatusCodes.BadIdentityTokenInvalid;
				}
				else
				{
					// construct translation object with default text.
					info = new TranslationInfo(
						"UntrustedCertificate",
						"en-US",
						"'{0}' is not a trusted user certificate.",
						certificate.Subject);
				}

				// create an exception with a vendor defined sub-code.
				throw new ServiceResultException(new ServiceResult(
					result,
					info.Key,
					LoadServerProperties().ProductUri,
					new LocalizedText(info)));
			}
		}

		#region Private Fields
		#endregion
		#endregion

	}
}
